// @author HuangYeWuDeng
// @date 2018-07-17
// @description simple tool to export github starred repos via public api
// the api doc can be found at https://developer.github.com/v3/activity/starring/#list-repositories-being-starred

package main

import (
	"fmt"
	"log"
	"net/http"
	"strings"

	"encoding/json"
	"io/ioutil"
	"strconv"
	"time"

	"github.com/astaxie/beego/logs"
	"github.com/globalsign/mgo/bson"
	"github.com/knq/ini"
	"github.com/pkg/errors"
)

var blog *logs.BeeLogger
var db *StarDb

var ghStarOnly bool
var ghCollection string
var ghUser string
var ghToken string
var ghStartPage int
var ghPerPage int
var ghSleep int

func init() {
	//init logger
	blog = logs.NewLogger(1024)
	blog.SetLogger("console", "")
	//blog.EnableFuncCallDepth(true)

	f, err := ini.LoadFile("config.ini")
	if err != nil {
		blog.Critical("can not load config file: %s\n", "config.ini")
	}

	g := f.GetSection("github")
	ghUser = g.Get("user")
	ghToken = g.Get("token")
	ghStarOnly, err = strconv.ParseBool(g.Get("star_only"))
	ghStartPage, err = strconv.Atoi(g.Get("start_page"))
	if err != nil || ghStartPage <= 0 {
		ghStartPage = 1
	}
	ghPerPage, err = strconv.Atoi(g.Get("per_page"))
	if err != nil || ghPerPage > 200 {
		ghPerPage = 100
	}
	ghSleep, err = strconv.Atoi(g.Get("sleep"))
	if err != nil || ghSleep < 100 {
		ghSleep = 500
	}
	//init db
	s := f.GetSection("mongo")
	user := s.Get("user")
	passwd := s.Get("passwd")
	host := s.Get("host")
	port := s.Get("port")
	dbname := s.Get("db_name")
	ghCollection = s.Get("collection")
	blog.Alert("mongo: get mongo config: user: %s, host: %s, port: %s\n", user, host, port)
	if !ghStarOnly {
		blog.Alert("mongo: save data to : %s.%s \n", dbname, ghCollection)
		db = New(user, passwd, host, port, dbname, "authSource=admin")
		blog.Alert("mongo: mongodb connection success")
	}
}

func httpClose(response *http.Response) {
	if nil != response && nil != response.Body {
		response.Body.Close()
	}
}

func httpRequest(method, url, oauthToken string) (*http.Response, error) {
	var resp *http.Response
	client := &http.Client{}
	client.Timeout = time.Second * 300
	request, err := http.NewRequest(method, url, strings.NewReader(""))
	if err != nil {
		return resp, err
	}
	if oauthToken != "" {
		token := fmt.Sprintf("token %s", oauthToken)
		request.Header.Add("Authorization", token)
	}
	request.ContentLength = 0
	resp, err = client.Do(request)
	return resp, err
}

func getOnePage(user, oauthToken string, perPage, page int) error {
	url := fmt.Sprintf("https://api.github.com/users/%s/starred?per_page=%d&page=%d",
		user, perPage, page)
	// res, err := http.Get(url)
	res, err := httpRequest("GET", url, oauthToken)
	if err != nil {
		panic(err)
	}
	defer httpClose(res)
	if res.StatusCode != 200 {
		errors.New(fmt.Sprintf("status code error: [%d], %s", res.StatusCode, res.Status))
	}
	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}

	var ghStarredRepos []GithubRepo
	err = json.Unmarshal(body, &ghStarredRepos)
	if err != nil {
		var ghErr GithubLimitErr
		gerr := json.Unmarshal(body, &ghErr)
		if gerr == nil {
			return errors.New(fmt.Sprintf("github Err: %s ,\n \t doc url: %s .",
				ghErr.Message, ghErr.DocumentationURL))
		}
		return errors.New(fmt.Sprintf("json.Unmarshal: %s, body: %d.",
			err.Error(), string(body)))
	}
	if len(ghStarredRepos) == 0 {
		return errors.New("not more starred repos found.")
	}

	rate := time.Second / 20
	burstLimit := 40
	tick := time.NewTicker(rate)
	defer tick.Stop()
	throttle := make(chan time.Time, burstLimit)
	go func() {
		for t := range tick.C {
			select {
			case throttle <- t:
			default:
			}
		} // does not exit after tick.Stop()
	}()

	//fmt.Printf("%+v\n", ghStarredRepos)
	for _, starred := range ghStarredRepos {
		<-throttle // rate limit our req
		go func(starred GithubRepo) {
			if !ghStarOnly {
				starred.ID = bson.NewObjectId()
				err := db.Insert(ghCollection, starred)
				if err != nil {
					panic(fmt.Sprintf("db.Insert Err: %s", err.Error()))
				}
				blog.Info("added to database: repo full_name: %s\n", starred.FullName)
			}
			starErr := githubStarRepo(ghToken, starred.FullName)
			if starErr == nil {
				blog.Info("star successfully. repo full_name: %s\n", starred.FullName)
			} else {
				panic(starErr)
			}
		}(starred)
	}
	return nil
}

func githubStarredScrape() {
	//https://api.github.com/users/ihacklog/starred?page=1&per_page=100
	page := ghStartPage
	for {
		blog.Alert("start to fetch page: %d, per_page: %d ======================>",
			page, ghPerPage)
		err := getOnePage(ghUser, ghToken, ghPerPage, page)
		if err != nil {
			blog.Notice("%s <====================== ", err.Error())
			break
		}
		blog.Alert("done fetch page: %d. sleep for %d ms <====================== \n\n",
			page, ghSleep)
		time.Sleep(time.Millisecond * time.Duration(ghSleep))
		page++
	}
}

//see https://developer.github.com/v3/activity/starring/#check-if-you-are-starring-a-repository
//GET /user/starred/:owner/:repo
//Status: 204 No Content Response if this repository is starred by you
//Status: 404 Not Found Response if this repository is not starred by you
func githubCheckStarring(oauthToken, repoFullname string) (bool, error) {
	url := fmt.Sprintf("https://api.github.com/user/starred/%s", repoFullname)
	res, err := httpRequest("GET", url, oauthToken)
	defer httpClose(res)
	if err == nil && res.StatusCode == 204 {
		return true, nil
	}
	if err == nil && res.StatusCode == 404 {
		return false, nil
	}
	return false, err
}

//star a repo
//see https://developer.github.com/v3/activity/starring/#star-a-repository
//PUT https://api.github.com/user/starred/:owner/:repo?access_token=xxxx
func githubStarRepo(oauthToken, repoFullName string) error {
	if oauthToken == "" {
		log.Fatal("github oauth token can not be empty!\n please get token from https://github.com/settings/tokens/new\n")
	}
	isStarring, err := githubCheckStarring(oauthToken, repoFullName)
	if err != nil {
		return err
	}
	if isStarring {
		blog.Alert("repo %s is starring, no need star again.", repoFullName)
		return nil
	}
	url := fmt.Sprintf("https://api.github.com/user/starred/%s", repoFullName)
	response, err := httpRequest("PUT", url, oauthToken)
	defer httpClose(response)
	if err == nil && response.StatusCode == 204 {
		return nil
	}
	return err
}

func main() {
	githubStarredScrape()
}
